/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.web30.nntp.sync;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.field.MailboxListField;
import org.apache.james.mime4j.field.UnstructuredField;
import org.apache.james.mime4j.message.Message;

import org.web30.nntp.Util;
import org.web30.nntp.service.NNTPHttpService;
import org.web30.nntp.service.NNTPService;
import org.web30.nntp.service.NNTPService.NewsRecords;
import org.web30.nntp.service.NNTPService.NewsWriter;

/**
 *
 * @author Xia Zhenyu
 */
public class SyncClientService implements Runnable {

    public static final String LAST_FILE_NAME = "sync.last";
    public static final String INTERNET_LAST_SYNCED_ARTICLE_ID = "Web30_Article_ID";
    public static final String INTRANET_LAST_SYNCED_ARTICLE_ID = "NNTP_Article_ID";
    public static final String REPLICA_HEADER = "ReplicaFrom: ";
    /**
     * Intranet NNTP Service
     */
    private NNTPService nativeNNTPService = null;
    /**
     * Internet NNTP Service which leverage NNTPBridge
     */
    private NNTPHttpService httpNNTPService = null;
    /**
     * The last synced article id for intranet NNTP service
     */
    private String intranetLastArticleId = null;
    /**
     * The last synced article id for internet NNTP service
     */
    private String internetLastArticleId = null;
    /**
     * Interval of sync process
     */
    private long interval = 30000;
    /**
     * Size limit of article
     */
    private long articleSizeLimit = 1024*1024;
    /**
     * Replica Marder added into header which post to internet NNTPBridge
     */
    private String syncID = "isReplica";
    /**
     * Group to sync
     */
    private String syncGroup = null;
    /**
     * Only post news to server, but doesn't retrive news from server
     */
    private boolean silentMode = true;


    public String getSyncGroup() {
        return syncGroup;
    }

    public void setSyncGroup(String syncGroup) {
        this.syncGroup = syncGroup;
    }

    public String getSyncID() {
        return syncID;
    }

    public void setSyncID(String syncID) {
        this.syncID = syncID;
    }
    /**
     * Constructor 
     * @param httpProxy
     * @param httpProxyPort
     * @param user
     * @param pwd
     * @param intranetNNTPHostName
     * @param intranetNNTPPort
     * @param NNTPBridgeURL
     */
    public SyncClientService(
            String httpProxy,
            int httpProxyPort,
            String user,
            String pwd,
            String intranetNNTPHostName,
            int intranetNNTPPort,
            String NNTPBridgeURL,
            String group,
            int interval) {

        this.nativeNNTPService = new NNTPService(
                intranetNNTPHostName,
                intranetNNTPPort,
                null,
                null);

        this.httpNNTPService = new NNTPHttpService(
                httpProxy,
                httpProxyPort,
                NNTPBridgeURL,
                user,
                pwd);
        this.syncGroup = group;
        this.interval = interval;
        this.initLastIds();
    }

    public long getInterval() {
        return interval;
    }

    public void setInterval(long interval) {
        this.interval = interval;
    }

    /**
     * Read last file which contains last id of sync
     */
    private void initLastIds() {
        try {
            File f = new File(getLastFileName());
            if (!f.exists()) {
                f.createNewFile();
                return;
            }
            // Open the file that is the first
            // command line parameter
            FileInputStream fstream = new FileInputStream(getLastFileName());
            // Get the object of DataInputStream
            DataInputStream in = new DataInputStream(fstream);
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String strLine = null;
            //Read File Line By Line
            while ((strLine = br.readLine()) != null) {
                // Print the content on the console
                this.getLastSyncArticleId(strLine);
            }
            //Close the input stream
            in.close();
        } catch (Exception e) {//Catch exception if any
            System.err.println("Error: " + e.getMessage());
        }
    }

    private void getLastSyncArticleId(String line) {
        if (line.startsWith(SyncClientService.INTERNET_LAST_SYNCED_ARTICLE_ID)) {
            this.internetLastArticleId = line.substring(line.indexOf(":") + 1).trim();
        } else if (line.startsWith(SyncClientService.INTRANET_LAST_SYNCED_ARTICLE_ID)) {
            this.intranetLastArticleId = line.substring(line.indexOf(":") + 1).trim();
        }
        return;
    }

    private String getLastFileName() {
        return this.syncGroup + "." + LAST_FILE_NAME;
    }

    private void updateLastSyncedIds() throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(getLastFileName()));
        if (this.intranetLastArticleId != null) {
            writer.write(SyncClientService.INTRANET_LAST_SYNCED_ARTICLE_ID + ":" + this.intranetLastArticleId);
            writer.newLine();
        }
        if (this.internetLastArticleId != null) {
            writer.write(SyncClientService.INTERNET_LAST_SYNCED_ARTICLE_ID + ":" + this.internetLastArticleId);
        }
        writer.close();
    }

    /**
     * Get news from intranet news server and post to NNTPBridge
     * @return
     * @throws IOException
     */
    public boolean intranet2Internet() throws IOException {
        this.nativeNNTPService.init();
        this.nativeNNTPService.setOmmitKey("ReplicaFrom");
        this.nativeNNTPService.setOmmitValue("isReplica");
        NewsRecords records = this.nativeNNTPService.getNewNews(this.syncGroup,
                this.intranetLastArticleId);
        String lastSyncId = null;
        boolean res = false;
        lastSyncId = records.getArticleId();
        while (records.hasNext()) {
            BufferedReader reader = records.next();
            BufferedReader filteredArticle = this.parseMIME(reader);
            if (filteredArticle == null) {
                continue;
            }
            res = this.httpNNTPService.postNews(syncGroup,
                    filteredArticle);
            if (!res) {
                break;
            } else {
                this.intranetLastArticleId = lastSyncId;
                this.updateLastSyncedIds();
            }
            lastSyncId = records.getArticleId();
        }
        this.nativeNNTPService.close();
        return res;
    }

    /**
     * Get news from NNTPBridge, and post to intranet NNTPService
     * @return
     * @throws IOException
     */
    public boolean internet2Intranet() throws IOException {
        String temp = null;
        if (this.internetLastArticleId == null) {
            temp = null;
        } else {
            temp = this.internetLastArticleId.substring(1, internetLastArticleId.length() - 1);
        }
        BufferedReader reader = this.httpNNTPService.getNewNews(this.syncGroup,
                temp);
        boolean res = true;
        while (reader != null) {
            reader = this.parseMIME(reader);
            try {
                if (reader != null) {
                    res = this.singlePost(reader);
                    if (!res) {
                        break;
                    } else {
                        this.internetLastArticleId = this.httpNNTPService.getLastMessageId();
                        this.updateLastSyncedIds();
                    }
                }
                //if reader is null, it will skip this entries
                reader = this.httpNNTPService.getNewNews(this.syncGroup,
                        this.internetLastArticleId.substring(1, internetLastArticleId.length() - 1));
            } catch (Exception e) {
                System.out.print(e);
            }
        }
        return res;
    }

    /**
     * Start sync process
     */
    public void syncProcess() {
        while (true) {
            try {
                this.intranet2Internet();
                if (!this.silentMode) {
                    this.internet2Intranet();
                }
                this.updateLastSyncedIds();
            } catch (IOException ex) {
                System.out.print(ex);
            }
            this.interval();
        }
    }

    /**
     * Post a single article to intranet NNTP Server
     * @param reader
     * @return
     * @throws SocketException
     * @throws IOException
     */
    public boolean singlePost(BufferedReader reader) {
        try {
            this.nativeNNTPService.init();
            NewsWriter res = this.nativeNNTPService.postNews(this.syncGroup);
            if (!res.hasNext()) {
                return false;
            }
            BufferedWriter writer = res.next();
            char[] buf = new char[1024];
            int read = 0;
            while ((read = reader.read(buf)) >= 0) {
                writer.write(buf, 0, read);
            }
            writer.close();
            reader.close();
            this.nativeNNTPService.close();
        } catch (SocketException ex) {
            Logger.getLogger(SyncClientService.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        } catch (IOException ex) {
            Logger.getLogger(SyncClientService.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
        return true;
    }

    private boolean isExceedLimitSize(long articleSize) {
        boolean ret = true;
        if (articleSize < this.articleSizeLimit) {
            ret = false;
        }
        return ret;
    }

    public void setArticleSizeLimit(long articleSizeLimit) {
        this.articleSizeLimit = articleSizeLimit;
    }

    public void setSilentMode(boolean silentMode) {
        this.silentMode = silentMode;
    }
    /**
     * Parse message check headers and size of article
     * @param in
     * @return
     */
    private BufferedReader parseMIME(BufferedReader in) {
        ByteArrayOutputStream out = null;
        try {
            out = new ByteArrayOutputStream();
            Util.pipe(in, out);
            if (this.isExceedLimitSize(out.size())) {
                out.close();
                return null;
            }
            ByteArrayInputStream msg = new ByteArrayInputStream(out.toByteArray());
            out.close();
            out = null;
            Message mime = new Message(msg);
            String from = mime.getHeader().getField("From").getBody();
            if (this.illegalFrom(from)) {
                from = this.getFromValue(from);
                mime.getHeader().removeFields("from");
                MailboxListField fromHeader = (MailboxListField) MailboxListField.parse("from: " + from);
                mime.getHeader().setField(fromHeader);
            }
            mime.getHeader().removeFields("x-trace");
            mime.getHeader().removeFields("x-complaints-to");
            mime.getHeader().removeFields("nntp-posting-date");
            mime.getHeader().removeFields("nntp-posting-host");
            mime.getHeader().removeFields("xref");
            mime.getHeader().removeFields("date");
            UnstructuredField replica = (UnstructuredField) UnstructuredField.parse(this.returnReplicaMarker());
            mime.getHeader().setField(replica);

            out = new ByteArrayOutputStream();
            mime.writeTo(out);
            return new BufferedReader(
                    new InputStreamReader(
                    new ByteArrayInputStream(out.toByteArray()), Util.DEFAULT_ENCODING));

        } catch (MimeException ex) {
            Logger.getLogger(SyncClientService.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        } catch (UnsupportedEncodingException ex) {
            Logger.getLogger(SyncClientService.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        } catch (IOException ex) {
            Logger.getLogger(SyncClientService.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }

    private String returnReplicaMarker() {
        return SyncClientService.REPLICA_HEADER + this.syncID;
    }

    private String getFromValue(String line) {
        //Value sample xiazy <xiazy@abc>
        int i = line.indexOf("<");
        if (i < 0) {
            return line.trim() + " <guest@web30.org>";
        } else if ( i == 0 ) {
            return "Guest <guest@web30.org>";
        } else {
            return line.substring(0, i - 1).trim() + " <guest@web30.org>";
        }
    }

    private boolean illegalFrom(String line) {
        int i = line.indexOf("@");
        if (i < 0) {
            return true;
        }
        if( line.indexOf("com") < 0 &&
            line.indexOf("org") < 0 &&
            line.indexOf("net") < 0 ) {
            return true;
        }
        return false;
    }

    /**
     * Interval Sleep default time
     */
    private void interval() {
        try {
            Thread.sleep(interval);
        } catch (InterruptedException ex) {
            Logger.getLogger(SyncClientService.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    //@Override
    //@Override
    public void run() {
        this.syncProcess();
    }

}
